package com.practice.chatroom;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.Socket;
import java.net.URLDecoder;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

import com.practice.chatroom.Deal.HttpStatusCode;

import org.apache.commons.lang3.StringUtils;

/** 本类专注于一些业务功能场景上的额外逻辑、与openjfx做的gui client交互、读写数据库 */
public class ChatRoomBus {

    private ChatRoomServer svr;
    private Socket socket4client;

    /**
     * 为浏览器请求初始化业务逻辑实例
     * 
     * @param svr 服务端
     */
    public ChatRoomBus(ChatRoomServer svr) {
        this.svr = svr;
    }

    /**
     * 为gui客户端的交互，初始化业务逻辑实例
     * 
     * @param svr           服务端
     * @param socket4client 交互socket
     * @param header        已解析的请求头
     */
    public ChatRoomBus(ChatRoomServer svr, Socket socket4client, List<String> header) {
        this.svr = svr;
        this.socket4client = socket4client;
        buildClientConn(header);
    }

    /**
     * 注册新用户业务逻辑。注意目前暂时未做gui客户端的注册功能
     * 
     * @param username  用户名
     * @param password  密码的RSA密文
     * @param icon      上传的图像图片，未启用数据库时为null
     * @param RSAmodule RSA模数
     * @return 本次注册请求的响应实例
     * @throws AppException 可能会抛给前台的错误提示
     * @see com.practice.chatroom.Resp
     */
    public Resp reg(String username, String password, String icon, String RSAmodule) throws AppException {
        if (svr.conf.getBoolean("withDB")) {
            authEmpty(username, password);
            try { // 往数据库新增注册的用户数据
                if (svr.dao.select("SELECT 1 FROM chat_room_user WHERE username = '" + username + "'").size() > 0) {
                    throw new AppException("用户已存在，请换个名！");
                }
                Class<?>[] colTypes = new Class<?>[] { String.class, String.class, byte[].class };
                String cols = "username,password,icon";
                String[] colNames = cols.split(",");
                String xorBase64pw = Base64.getEncoder()
                        .encodeToString(xorPwByUsername(username, rebuildPassword(password, RSAmodule))); // 将密码先按RSA分段解密为原文，再按自行设计的简易加密写入数据库
                List<Map<String, Object>> list = List.of(
                        Map.of(colNames[0], username, colNames[1], xorBase64pw, colNames[2], icon.getBytes("UTF-8")));
                String sql = "INSERT INTO chat_room_user (" + cols + ") VALUES(?, ?, ?);";
                List<Map<String, Object>> newKeys = new ArrayList<>();
                svr.dao.insertBatch(sql, list, colNames, colTypes, new String[] { "userid" }, newKeys);
                svr.logger.info(String.format("向数据库新插了用户记录 用户名：%s 密码密文：%s%n", username, xorBase64pw));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        return new Resp(HttpStatusCode.OK_200, null);
    }

    /**
     * 登录的业务逻辑
     * 
     * @param username  用户名
     * @param password  密码，浏览器时为密文，gui客户端时就是正常串
     * @param RSAmodule 浏览器时为访问登录页所获取到的随机RSA新模数，gui客户端时为空
     * @return 本次登录请求的响应实例，gui客户端时返回null
     * @throws AppException 可能会抛给前台的错误提示
     * @see com.practice.chatroom.Resp
     */
    public Resp login(String username, String password, String RSAmodule) throws AppException {
        authEmpty(username, password);
        String icon = null;
        if (svr.conf.getBoolean("withDB")) { // 启用数据库时，验证用户信息
            List<Map<String, Object>> rs = svr.dao
                    .select("SELECT password,icon FROM chat_room_user WHERE username = '" + username + "'");
            if (rs.size() < 1) {
                throw new AppException("用户不存在！");
            }
            try {
                String clearByRSA = password;
                if (null == socket4client) { // 当被浏览器请求调用时，需先解密RSA
                    clearByRSA = new String(rebuildPassword(password, RSAmodule), "UTF-8");
                    svr.logger.info(String.format("密码密文 %s 分段RSA解密为原文：%s%n", password, clearByRSA));
                }
                byte[] bytes = Base64.getDecoder().decode((String) rs.get(0).get("password"));
                String clearByDB = new String(xorPwByUsername(username, bytes), "UTF-8"); // 异或两次等于还原
                if (!clearByDB.equals(clearByRSA)) {
                    throw new AppException("密码错误！");
                }
                icon = new String((byte[]) rs.get(0).get("icon")); // 从数据库取出注册时的头像图片数据
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        } else { // 未开启数据库时，统一使用gui客户端图标文件当头像。此时密码也没有意义，不做任何校验
            try (FileInputStream fis = new FileInputStream(new File("./src/main/resources/imgs/D.png"))) {
                icon = "data:image/png;base64," + Base64.getEncoder().encodeToString(fis.readAllBytes());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        String sessionId = svr.onlineUser(username, icon, RSAmodule, socket4client); // 调用服务端的公共上线方法
        Resp resp = null;
        if (null == socket4client) { // 当被浏览器请求调用时，这个肯定为空
            resp = new Resp(HttpStatusCode.LOCATION_302, "/webapp/pages/chatRoom.html");
            resp.setCookie("sessionId", sessionId);
            resp.setCookie("username", username);
        }
        if (svr.conf.getBoolean("withDB")) { // 更新上次登录时间、客户端类型
            Class<?>[] colTypes = new Class<?>[] { Timestamp.class, Character.class, String.class };
            String[] colNames = "last_login,last_client,username".split(",");
            Map<String, Object> map = new HashMap<>();
            Timestamp now = new Timestamp(System.currentTimeMillis());
            map.put(colNames[0], now);
            char clientType = null == socket4client ? '0' : '1';
            map.put(colNames[1], clientType);
            map.put(colNames[2], username);
            String sql = "UPDATE chat_room_user SET last_login = ?, last_client = ? WHERE username = ?;";
            svr.dao.update(sql, map, colNames, colTypes);
            svr.logger.info(String.format("数据库更新用户 %s 上次登录时间为：%s 上次登录的客户端类型为：%s%n", username, now, clientType));
        }
        return resp;
    }

    /**
     * 发新聊天消息的业务逻辑
     * 
     * @param username 发送消息的用户
     * @param isImg    是否为图片消息
     * @param msg      消息内容，图片时为其base64串
     * @param color    浏览器端支持自定义字色
     * @param fontSize 浏览器端支持自定义字号
     * @return 本次发消息请求的响应实例
     * @see com.practice.chatroom.Resp
     */
    public Resp sendMsg(String username, boolean isImg, String msg, String color, String fontSize) {
        Resp resp = new Resp(HttpStatusCode.NO_CONTENT_204, null);
        svr.spreadMsg(username, isImg, msg, color, fontSize + "px"); // 调用服务端的发消息公共方法
        if (svr.conf.getBoolean("withDB")) {
            // TODO 数据库保存漫游聊天记录
        }
        return resp;
    }

    /**
     * 登出业务逻辑。注意gui客户端还未做这种正常登出，只能直接关闭程序，因而暂不会触此方法
     * 
     * @param sessionId 会话id
     * @return 本次登出请求的响应实例
     * @throws AppException 可能会抛给前台的错误提示
     * @see com.practice.chatroom.Resp
     */
    public Resp exit(String sessionId) throws AppException {
        svr.logger.info(String.format("会话 %s 主动操作了登出%n", sessionId));
        Resp resp = new Resp(HttpStatusCode.LOCATION_302, "/");
        svr.offline(sessionId, null, true);
        return resp;
    }

    /**
     * 建立与gui客户端的长连接，保与其交互
     * 
     * @param header 已解析的请求头
     */
    private void buildClientConn(List<String> header) {
        try {
            String username = null;
            try {
                if (header.size() < 2) {
                    throw new AppException("客户端没有按建立连接的规则发送数据！");
                }
                Map<String, String> loginData = svr.analysisStr(header.get(1), "&", "=");
                Scanner socketSc = new Scanner(socket4client.getInputStream(), "UTF-8");
                username = loginData.get("username");
                login(username, loginData.get("password"), null); // 共用上线逻辑
                svr.writeToClient(username, "{msgType:'logined',datas:{}}"); // 完成初次交互，视为登录成功
                svr.pushOnlineUser(); // 浏览器端在ws建立后才单独刷新在线用户信息，gui客户端握手后即可触发
                socketSc.nextLine();
                while (socketSc.hasNextLine()) { // 持续等待gui客户端发起交互
                    svr.spreadMsg(username, false, URLDecoder.decode(socketSc.nextLine(), "UTF-8"), "black", "12"); // 目前就只有发消息这一种情况
                }
                socketSc.close(); // 走到这句时说明连接已断开，视为关闭了程序，则应做下线处理
                svr.offline(username, null, false); // 调用服务端登出公共方法
            } catch (AppException e) {
                e.printStackTrace();
                svr.writeToClient(socket4client.getOutputStream(), "{msgType:'error',errMsg:'" + e.getMessage() + "'}");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 用户名密码非空校验
     * 
     * @param username 用户名
     * @param password 密码
     * @throws AppException 有空串时要抛给前台的错误
     */
    private void authEmpty(String username, String password) throws AppException {
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            throw new AppException("用户名和密码都不能为空！");
        }
    }

    /**
     * 根据密文及RSA模重建密码串的ASCII数组
     * 
     * @param password  密码密文
     * @param RSAmodule RSA模数
     * @return 明文ASCII数组
     * @throws AppException 可能会抛给前台的错误提示
     */
    private byte[] rebuildPassword(String password, String RSAmodule) throws AppException {
        int blockL = RSAmodule.length(); // 单段长度，分段方面的规则前端要一致
        String multiBlock = "";
        for (int i = 0, maxL = password.length(); i < maxL; i += blockL) { // 本应用RSA模数不大，密码长了需分段处理
            BigInteger blockCipher = new BigInteger(password.substring(i, i + blockL)); // 约定每段密文保持模数长度不足补0
            BigInteger blockClear = svr.decodeRSA(new BigInteger(RSAmodule), blockCipher); // 单段密文解密为原文，转化类型时开头的0还是可能被抛弃掉
            multiBlock += String.format("%0" + (blockL - 1) + "d", blockClear); // 注意原文补齐时只到RSA模的位数-1，因分段时要确保比模数小
        }
        String clearCode = new BigInteger(multiBlock).toString();
        int l = clearCode.length();
        if (l % 3 > 0) { // 类似先前，转化类型去掉补模数位的0时，可能包含了其实是ASCII串自身首个字符开头有0
            clearCode = "0".repeat(3 - l % 3) + clearCode; // 约定每三位对应个字符不足补0
            l = clearCode.length();
        }
        byte[] pw = new byte[l / 3];
        for (int i = 0; i < l; i += 3) { // ASCII还原为字符
            pw[i / 3] = (byte) Integer.parseInt(clearCode.substring(i, i + 3));
        }
        return pw;
    }

    /**
     * 密码存取数据库时简单做个与用户名有关的异或加密
     * 
     * @param username 用户名
     * @param password 原文字节流
     * @return 异或后的字节流
     * @throws UnsupportedEncodingException utf8转换异常
     */
    private byte[] xorPwByUsername(String username, byte[] password) throws UnsupportedEncodingException {
        BigInteger userCodes = new BigInteger(username.getBytes("UTF-8"));
        byte[] pow10high = Arrays.copyOf(userCodes.pow(79).toByteArray(), 10); // 为防用户名太短失去效果，取其幂数的最高10位
        return (new BigInteger(password)).xor(new BigInteger(pow10high)).toByteArray();
    }
}